﻿Imports System.Threading

Public Class UITimeRuler


    Const MaxBars As Integer = 8
    Const MaxSamples As Integer = 256
    Const MaxNestCall As Integer = 32
    Const MaxSampleFrames As Integer = 4
    Const LogSnapDuration As Integer = 120
    Const BarHeight As Integer = 8
    Const BarPadding As Integer = 2
    Const AutoAdjustDelay As Integer = 30

    Public Property ShowLog() As Boolean
        Get
            Return m_ShowLog
        End Get
        Set(value As Boolean)
            m_ShowLog = value
        End Set
    End Property
    Private m_ShowLog As Boolean

    Public Property TargetSampleFrames() As Integer
        Get
            Return m_TargetSampleFrames
        End Get
        Set(value As Integer)
            m_TargetSampleFrames = value
        End Set
    End Property
    Private m_TargetSampleFrames As Integer

    Public Property Position() As Vector2
        Get
            Return m_position
        End Get
        Set(value As Vector2)
            m_position = value
        End Set
    End Property

    Public Property Width() As Integer
        Get
            Return m_Width
        End Get
        Set(value As Integer)
            m_Width = value
        End Set
    End Property
    Private m_Width As Integer

    Private Structure Marker
        Public MarkerId As Integer
        Public BeginTime As Single
        Public EndTime As Single
        Public Color As Color
    End Structure

    Private Class MarkerCollection
        ' Marker collection.
        Public Markers As Marker() = New Marker(MaxSamples - 1) {}
        Public MarkCount As Integer

        ' Marker nest information.
        Public MarkerNests As Integer() = New Integer(MaxNestCall - 1) {}
        Public NestCount As Integer
    End Class

    Private Class FrameLog
        Public Bars As MarkerCollection()

        Public Sub New()
            ' Initialize markers.
            Bars = New MarkerCollection(MaxBars - 1) {}
            For i As Integer = 0 To MaxBars - 1
                Bars(i) = New MarkerCollection()
            Next
        End Sub
    End Class

    Private Class MarkerInfo
        Public Name As String
        Public Logs As MarkerLog() = New MarkerLog(MaxBars - 1) {}
        Public Sub New(name__1 As String)
            Name = name__1
        End Sub
    End Class

    Private Structure MarkerLog
        Public SnapMin As Single
        Public SnapMax As Single
        Public SnapAvg As Single

        Public Min As Single
        Public Max As Single
        Public Avg As Single

        Public Samples As Integer

        Public Color As Color

        Public Initialized As Boolean
    End Structure

    Private logs As FrameLog()
    Private prevLog As FrameLog
    Private curLog As FrameLog
    Private frameCount As Integer
    Private stopwatch As New Stopwatch()
    Private markers As New List(Of MarkerInfo)()
    Private markerNameToIdMap As New Dictionary(Of String, Integer)()
    Private frameAdjust As Integer
    Private sampleFrames As Integer
    Private logString As String = ""
    Private updateCount As Integer
    Public UserInterface As UserInterface
    Private m_position As Vector2

    Public Font As SpriteFont
    Public Background As Texture2D


    Public Sub New(UserInterface As UserInterface)
        Me.UserInterface = UserInterface
        Font = UserInterface.TextureManager.GetTexture("Default").Font
        Background = UserInterface.TextureManager.GetTexture("Default").Texture2D
        logs = New FrameLog(1) {}
        For i As Integer = 0 To logs.Length - 1
            logs(i) = New FrameLog()
        Next
        sampleFrames = InlineAssignHelper(TargetSampleFrames, 1)
        Width = CInt(UserInterface.Width * 0.8F)
        Dim layout As New Layout(UserInterface.ViewPort) 'TODO: Remove this!
        m_position = layout.Place(New Vector2(Width, BarHeight), 0, 0.01F, Alignment.BottomCenter)
        ShowLog = True
        Interlocked.Exchange(updateCount, 0)
    End Sub

    Private Sub CommandExecute(command As String, arguments As IList(Of String))
        'Dim previousVisible As Boolean = Visible
        'If arguments.Count = 0 Then
        '    Visible = Not Visible
        'End If

        Dim subArgSeparator As Char() = {":"c} 'New ()  {":"c}
        For Each orgArg As String In arguments
            Dim arg As String = orgArg.ToLower()
            Dim subargs As String() = arg.Split(subArgSeparator)
            Select Case subargs(0)
                Case "on"
                    'Visible = True
                    Exit Select
                Case "off"
                    'Visible = False
                    Exit Select
                Case "reset"
                    ResetLog()
                    Exit Select
                Case "log"
                    If subargs.Length > 1 Then
                        If [String].Compare(subargs(1), "on") = 0 Then
                            ShowLog = True
                        End If
                        If [String].Compare(subargs(1), "off") = 0 Then
                            ShowLog = False
                        End If
                    Else
                        ShowLog = Not ShowLog
                    End If
                    Exit Select
                Case "frame"
                    Dim a As Integer = Int32.Parse(subargs(1))
                    a = Math.Max(a, 1)
                    a = Math.Min(a, MaxSampleFrames)
                    TargetSampleFrames = a
                    Exit Select
                Case "/?", "--help"
                    UserInterface.UICommandBox.Echo("tr [log|on|off|reset|frame]")
                    UserInterface.UICommandBox.Echo("Options:")
                    UserInterface.UICommandBox.Echo("       on     Display TimeRuler.")
                    UserInterface.UICommandBox.Echo("       off    Hide TimeRuler.")
                    UserInterface.UICommandBox.Echo("       log    Show/Hide marker log.")
                    UserInterface.UICommandBox.Echo("       reset  Reset marker log.")
                    UserInterface.UICommandBox.Echo("       frame:sampleFrames")
                    UserInterface.UICommandBox.Echo("              Change target sample frame count")
                    Exit Select
                Case Else
                    Exit Select
            End Select
        Next
        '   If Visible <> previousVisible Then
        Interlocked.Exchange(updateCount, 0)
        '   End If
    End Sub

    Public Sub StartFrame()
        SyncLock Me
            Dim count As Integer = Interlocked.Increment(updateCount)
            If 1 < count AndAlso count < MaxSampleFrames Then
                Return
            End If
            prevLog = logs(System.Math.Max(System.Threading.Interlocked.Increment(frameCount), frameCount - 1) And &H1)
            curLog = logs(frameCount And &H1)

            Dim endFrameTime As Single = CSng(stopwatch.Elapsed.TotalMilliseconds)
            For barIdx As Integer = 0 To prevLog.Bars.Length - 1
                Dim prevBar As MarkerCollection = prevLog.Bars(barIdx)
                Dim nextBar As MarkerCollection = curLog.Bars(barIdx)
                For nest As Integer = 0 To prevBar.NestCount - 1
                    Dim markerIdx As Integer = prevBar.MarkerNests(nest)

                    prevBar.Markers(markerIdx).EndTime = endFrameTime

                    nextBar.MarkerNests(nest) = nest
                    nextBar.Markers(nest).MarkerId = prevBar.Markers(markerIdx).MarkerId
                    nextBar.Markers(nest).BeginTime = 0
                    nextBar.Markers(nest).EndTime = -1
                    nextBar.Markers(nest).Color = prevBar.Markers(markerIdx).Color
                Next
                For markerIdx As Integer = 0 To prevBar.MarkCount - 1
                    Dim duration As Single = prevBar.Markers(markerIdx).EndTime - prevBar.Markers(markerIdx).BeginTime

                    Dim markerId As Integer = prevBar.Markers(markerIdx).MarkerId
                    Dim m As MarkerInfo = markers(markerId)

                    m.Logs(barIdx).Color = prevBar.Markers(markerIdx).Color

                    If Not m.Logs(barIdx).Initialized Then
                        ' First frame process.
                        m.Logs(barIdx).Min = duration
                        m.Logs(barIdx).Max = duration
                        m.Logs(barIdx).Avg = duration

                        m.Logs(barIdx).Initialized = True
                    Else
                        ' Process after first frame.
                        m.Logs(barIdx).Min = Math.Min(m.Logs(barIdx).Min, duration)
                        m.Logs(barIdx).Max = Math.Min(m.Logs(barIdx).Max, duration)
                        m.Logs(barIdx).Avg += duration
                        m.Logs(barIdx).Avg *= 0.5F

                        If System.Math.Max(System.Threading.Interlocked.Increment(m.Logs(barIdx).Samples), m.Logs(barIdx).Samples - 1) >= LogSnapDuration Then
                            m.Logs(barIdx).SnapMin = m.Logs(barIdx).Min
                            m.Logs(barIdx).SnapMax = m.Logs(barIdx).Max
                            m.Logs(barIdx).SnapAvg = m.Logs(barIdx).Avg
                            m.Logs(barIdx).Samples = 0
                        End If
                    End If
                Next

                nextBar.MarkCount = prevBar.NestCount
                nextBar.NestCount = prevBar.NestCount
            Next

            ' Start measuring.
            stopwatch.Reset()
            stopwatch.Start()
        End SyncLock
    End Sub

    Public Sub BeginMark(markerName As String, color As Color)
        BeginMark(0, markerName, color)
    End Sub

    Public Sub BeginMark(barIndex As Integer, markerName As String, color As Color)
        SyncLock Me
            If barIndex < 0 OrElse barIndex >= MaxBars Then
                Throw New ArgumentOutOfRangeException("barIndex")
            End If

            Dim bar As MarkerCollection = curLog.Bars(barIndex)

            If bar.MarkCount >= MaxSamples Then
                Throw New OverflowException("Exceeded sample count." & vbLf + "Either set larger number to TimeRuler.MaxSmpale or" + "lower sample count.")
            End If

            If bar.NestCount >= MaxNestCall Then
                Throw New OverflowException("Exceeded nest count." & vbLf + "Either set larget number to TimeRuler.MaxNestCall or" + "lower nest calls.")
            End If

            ' Gets registered marker.
            Dim markerId As Integer
            If Not markerNameToIdMap.TryGetValue(markerName, markerId) Then
                ' Register this if this marker is not registered.
                markerId = markers.Count
                markerNameToIdMap.Add(markerName, markerId)
                markers.Add(New MarkerInfo(markerName))
            End If

            ' Start measuring.
            bar.MarkerNests(System.Math.Max(System.Threading.Interlocked.Increment(bar.NestCount), bar.NestCount - 1)) = bar.MarkCount

            ' Fill marker parameters.
            bar.Markers(bar.MarkCount).MarkerId = markerId
            bar.Markers(bar.MarkCount).Color = color
            bar.Markers(bar.MarkCount).BeginTime = CSng(stopwatch.Elapsed.TotalMilliseconds)

            bar.Markers(bar.MarkCount).EndTime = -1

            bar.MarkCount += 1
        End SyncLock
    End Sub

    Public Sub EndMark(markerName As String)
        EndMark(0, markerName)
    End Sub

    Public Sub EndMark(barIndex As Integer, markerName As String)
        SyncLock Me
            If barIndex < 0 OrElse barIndex >= MaxBars Then
                Throw New ArgumentOutOfRangeException("barIndex")
            End If

            Dim bar As MarkerCollection = curLog.Bars(barIndex)

            If bar.NestCount <= 0 Then
                Throw New InvalidOperationException("Call BeingMark method before call EndMark method.")
            End If

            Dim markerId As Integer
            If Not markerNameToIdMap.TryGetValue(markerName, markerId) Then
                Throw New InvalidOperationException([String].Format("Maker '{0}' is not registered." + "Make sure you specifed same name as you used for BeginMark" + " method.", markerName))
            End If

            Dim markerIdx As Integer = bar.MarkerNests(System.Threading.Interlocked.Decrement(bar.NestCount))
            If bar.Markers(markerIdx).MarkerId <> markerId Then
                Throw New InvalidOperationException("Incorrect call order of BeginMark/EndMark method." + "You call it like BeginMark(A), BeginMark(B), EndMark(B), EndMark(A)" + " But you can't call it like " + "BeginMark(A), BeginMark(B), EndMark(A), EndMark(B).")
            End If

            bar.Markers(markerIdx).EndTime = CSng(stopwatch.Elapsed.TotalMilliseconds)
        End SyncLock
    End Sub

    Public Function GetAverageTime(barIndex As Integer, markerName As String) As Single
        If barIndex < 0 OrElse barIndex >= MaxBars Then
            Throw New ArgumentOutOfRangeException("barIndex")
        End If

        Dim result As Single = 0
        Dim markerId As Integer
        If markerNameToIdMap.TryGetValue(markerName, markerId) Then
            result = markers(markerId).Logs(barIndex).Avg
        End If

        Return result
        '#Else
        '			Return 0F
        '#End If
    End Function

    Public Sub ResetLog()
        SyncLock Me
            For Each markerInfo As MarkerInfo In markers
                For i As Integer = 0 To markerInfo.Logs.Length - 1
                    markerInfo.Logs(i).Initialized = False
                    markerInfo.Logs(i).SnapMin = 0
                    markerInfo.Logs(i).SnapMax = 0
                    markerInfo.Logs(i).SnapAvg = 0

                    markerInfo.Logs(i).Min = 0
                    markerInfo.Logs(i).Max = 0
                    markerInfo.Logs(i).Avg = 0

                    markerInfo.Logs(i).Samples = 0
                Next
            Next
        End SyncLock
    End Sub

    Public Sub Draw(SpriteBatch As SpriteBatch)
        Draw(SpriteBatch, m_position, Width)
    End Sub

    Public Sub Draw(SpriteBatch As SpriteBatch, position As Vector2, width As Integer)
        ' Reset update count.
        Interlocked.Exchange(updateCount, 0)

        ' Gets SpriteBatch, SpriteFont, and WhiteTexture from DebugManager.

        ' Adjust size and position based of number of bars we should draw.
        Dim height As Integer = 0
        Dim maxTime As Single = 0
        For Each bar As MarkerCollection In prevLog.Bars
            If bar.MarkCount > 0 Then
                height += BarHeight + BarPadding * 2
                maxTime = Math.Max(maxTime, bar.Markers(bar.MarkCount - 1).EndTime)
            End If
        Next

        ' Auto display frame adjustment.
        ' For example, if the entire process of frame doesn't finish in less than 16.6ms
        ' thin it will adjust display frame duration as 33.3ms.
        Const frameSpan As Single = 1.0F / 60.0F * 1000.0F
        Dim sampleSpan As Single = CSng(sampleFrames) * frameSpan

        If maxTime > sampleSpan Then
            frameAdjust = Math.Max(0, frameAdjust) + 1
        Else
            frameAdjust = Math.Min(0, frameAdjust) - 1
        End If

        If Math.Abs(frameAdjust) > AutoAdjustDelay Then
            sampleFrames = Math.Min(MaxSampleFrames, sampleFrames)
            sampleFrames = Math.Max(TargetSampleFrames, CInt(maxTime / frameSpan) + 1)

            frameAdjust = 0
        End If

        ' Compute factor that converts from ms to pixel.
        Dim msToPs As Single = CSng(width) / sampleSpan

        ' Draw start position.
        Dim startY As Integer = CInt(position.Y) - (height - BarHeight)

        ' Current y position.
        Dim y As Integer = startY


        ' Draw transparency background.
        Dim rc As New Rectangle(CInt(position.X), y, width, height)
        SpriteBatch.Draw(Background, rc, New Color(0, 0, 0, 128))

        ' Draw markers for each bars.
        rc.Height = BarHeight
        For Each bar As MarkerCollection In prevLog.Bars
            rc.Y = y + BarPadding
            If bar.MarkCount > 0 Then
                For j As Integer = 0 To bar.MarkCount - 1
                    Dim bt As Single = bar.Markers(j).BeginTime
                    Dim et As Single = bar.Markers(j).EndTime
                    Dim sx As Integer = CInt(position.X + bt * msToPs)
                    Dim ex As Integer = CInt(position.X + et * msToPs)
                    rc.X = sx
                    rc.Width = Math.Max(ex - sx, 1)

                    SpriteBatch.Draw(Background, rc, bar.Markers(j).Color)
                Next
            End If

            y += BarHeight + BarPadding
        Next

        ' Draw grid lines.
        ' Each grid represents ms.
        rc = New Rectangle(CInt(position.X), CInt(startY), 1, height)
        Dim t As Single = 1.0F
        While t < sampleSpan
            rc.X = CInt(position.X + t * msToPs)
            SpriteBatch.Draw(Background, rc, Color.Gray)
            t += 1.0F
        End While

        ' Draw frame grid.
        For i As Integer = 0 To sampleFrames
            rc.X = CInt(position.X + frameSpan * CSng(i) * msToPs)
            SpriteBatch.Draw(Background, rc, Color.White)
        Next

        ' Draw log.
        If ShowLog Then
            ' Generate log string.
            y = startY - font.LineSpacing
            logString = ""
            For Each markerInfo As MarkerInfo In markers
                For i As Integer = 0 To MaxBars - 1
                    If markerInfo.Logs(i).Initialized Then
                        If logString.Length > 0 Then
                            logString += vbLf
                        End If
                        logString += " Bar " & i.ToString & " " & markerInfo.Name & " Avg.:" & markerInfo.Logs(i).SnapAvg.ToString & "ms "
                        y -= font.LineSpacing
                    End If
                Next
            Next

            ' Compute background size and draw it.
            Dim size As Vector2 = font.MeasureString(logString)
            rc = New Rectangle(CInt(position.X), CInt(y), CInt(size.X) + 12, CInt(size.Y))
            SpriteBatch.Draw(Background, rc, New Color(0, 0, 0, 128))

            ' Draw log string.
            spriteBatch.DrawString(font, logString, New Vector2(position.X + 12, y), Color.White)


            ' Draw log color boxes.
            y += CInt(CSng(font.LineSpacing) * 0.3F)
            rc = New Rectangle(CInt(position.X) + 4, y, 10, 10)
            Dim rc2 As New Rectangle(CInt(position.X) + 5, y + 1, 8, 8)
            For Each markerInfo As MarkerInfo In markers
                For i As Integer = 0 To MaxBars - 1
                    If markerInfo.Logs(i).Initialized Then
                        rc.Y = y
                        rc2.Y = y + 1
                        SpriteBatch.Draw(Background, rc, Color.White)
                        SpriteBatch.Draw(Background, rc2, markerInfo.Logs(i).Color)

                        y += font.LineSpacing
                    End If
                Next


            Next
        End If
    End Sub
    Private Shared Function InlineAssignHelper(Of T)(ByRef target As T, value As T) As T
        target = value
        Return value
    End Function

End Class